﻿using System;
using System.Runtime.Serialization;
using System.Text;
using CI.Interfaces;

namespace CI.Organism
{
    /// <summary>
    /// Connects Input Sensors, Neuron, Motor Outputs
    /// </summary>
    [DataContract]
    public class SimpleSynapse : Synapse
    {
        private static SimEnv SimEnv { get { return SimEnv.Instance; } }

        /// <summary>
        /// Constructor which will save its guid on creation
        /// </summary>
        /// <param name="guid">Synapes GUID</param>
        /// <param name="neuralSystem">Neural system it belongs to</param>
        public SimpleSynapse(Guid guid, INeuralSystem neuralSystem)
        {
            #region Argument Checks
            if (guid == Guid.Empty)
                throw new ArgumentException("guid");
            if (neuralSystem == null)
                throw new ArgumentException("neuralSystem");
            #endregion

            Guid = guid;
            NeuralSystem = neuralSystem;
        }

        /// <summary>
        /// Constructor which will save its guid on creation
        /// </summary>
        /// <param name="guid">Synapes GUID</param>
        /// <param name="neuralSystem">Neural system it belongs to</param>
        /// <param name="learningRule">Learning rule for the synapse</param>
        public SimpleSynapse(Guid guid, INeuralSystem neuralSystem, ILearningRule learningRule)
            : this(guid, neuralSystem)
        {
            #region Argument Checks
            if (learningRule == null)
                throw new ArgumentException("learningRule");
            #endregion

            LearningRule = learningRule;
        }

        /// <summary>
        /// Called each neural clock tick cycle to calculate and populate the output value and call any delegates
        /// </summary>
        public override bool Evaluate(UInt32 neuralHB)
        {
            // if we have already evaluted this organism heartbeat then dont do it again and return the last result
            lock (this)
            {
                if (LastEvalHeartbeatCount.HasValue && LastEvalHeartbeatCount == neuralHB)
                {
                    _outputValue = _lastEvalOutputValue;
                    RaiseEvaluationComplete();
                    return _lastEvalResult;
                }


                // we are evaluating so save the current heartbeat count
                LastEvalHeartbeatCount = neuralHB;

                // if we still have to wait before we can apply the input then do so
                if (InputWait > 0)
                {
                    // decrement input wait
                    InputWait--;
                    _outputValue = _lastEvalOutputValue = 0;
                    _lastEvalResult = false;
                    RaiseEvaluationComplete();
                    return _lastEvalResult;
                }


                // reset the output hold count when we have a new input value
                if ((InputValue > 0) && (OutputHold > 0))
                {
                    // reset the count
                    OutputHoldCount = OutputHold + 1;
                }

                // if we are holding the output value and it has already been calcuated
                if (OutputHoldCount > 0)
                {
                    // decrement the counter
                    OutputHoldCount--;

                    if (TargetNotification is INeuron)
                    {
                        NeuralSystem.AddToWaitingList(TargetNotification as INeuron);
                    }

                    if (OutputHoldCount != OutputHold)
                    {
                        // return the pre calculated result
                        _outputValue = _lastEvalOutputValue;
                        RaiseEvaluationComplete();

                        return _lastEvalResult;
                    }
                }
                else if (OutputHold > 0)
                // clear everything and exit once we get to 0 only if this is a holding synapse
                {
                    _outputValue = _lastEvalOutputValue = 0;
                    RaiseEvaluationComplete();
                    return false;
                }

                // check for bias
                if (InputWeight != 0)
                {
                    try
                    {
                        // set the output value to a multiplication of the input value
                        _outputValue = (long)(Math.Round(InputValue * InputWeight));
                    }
                    // if we are going to create a overflow
                    catch (ArithmeticException)
                    {
                        // if its going to be a positive overflow
                        if ((InputValue > 0) && (InputWeight > 0))
                            _outputValue = long.MaxValue;

                        // if its going to be a negative overflow
                        if ((InputValue < 0) && (InputWeight < 0))
                            _outputValue = long.MinValue;

                    }
                }
                else
                {
                    // set the output value as the input value
                    _outputValue = InputValue;
                }


                // clear the input value
                InputValue = 0;

                _lastEvalResult = true;
                _lastEvalOutputValue = _outputValue;
                RaiseEvaluationComplete();
                return _lastEvalResult;
            }
        }

        /// <summary>
        /// Fire the synapse, notify it that a potential has arrived and the value.
        /// This will add any neurons connected to this output synapse that we want to the waiting list
        /// </summary>
        /// <param name="inputValue">The input value/potential</param>
        public override void FireInput(long inputValue)
        {
            // save the input value into the input
            InputValue = inputValue;

            SimEnv.TraceLogger.LogDebug(string.Format("Event has fired on synapse {0} with value {1}", Guid, inputValue));

            // if it has a neuron/source tied to it notify it
            if (TargetNotification != null)
                TargetNotification.FireInput(inputValue);



            // if their is anything else listening
            if (ActivationEvent != null)
                ActivationEvent(inputValue);
        }

        public override event Action<long> ActivationEvent;


        /// <summary>
        /// Take this parent synapse and create a new synapse to be put in the new child Organism
        /// Mutate the current synapse and return a new one, deep class level mutation
        /// Also mutate the learning rule
        /// </summary>
        /// <param name="mutationRate">mutation rate to be applied</param>
        /// <param name="childOrganism">child Organism it will be going into</param>
        /// <returns>new synapse for child</returns>
        public override ISynapse Mutate(double mutationRate, IOrganism childOrganism)
        {
            #region Argument Checks
            if (childOrganism == null)
                throw new ArgumentException("childOrganism");
            if (childOrganism.NeuralSystem == null)
                throw new ArgumentException("childOrganism has no Neural System");
            if ((childOrganism.Cells == null) || (childOrganism.Cells.Count <= 0))
                throw new ArgumentException("childOrganism has not enough Neural Cells");
            #endregion

            // create the new synapse
            var synapse = new SimpleSynapse(Guid, childOrganism.NeuralSystem);

            // mutate the input weight
            synapse.InitInputWeight = Mutation.MutateCrawlOrLeap(InitInputWeight, mutationRate);

            // mutate the synapse output hold
            synapse.OutputHold = Mutation.MutateCrawlOrLeap(OutputHold, mutationRate);

            // if the target neuron is set and the binding name is set we cant really change the source
            MutateSource(childOrganism, mutationRate, synapse);

            // if the source name is set and the binding name is set we cant really change the target
            MutateTarget(childOrganism, mutationRate, synapse);


            return synapse;
        }

        private void MutateTarget(IOrganism childOrganism, double mutationRate, ISynapse synapse)
        {
            // if we are going to mutuate choose between neurons and SyapseIO values
            if (Mutation.MutateState(mutationRate))
            {

                // check against binding name
                if (BindSynapseToSynapseIOInsteadOfNeuron(childOrganism, synapse, synapse.TargetNotification as INeuron))
                {
                    synapse.AddSynapticConnectionTarget(GetRandomSynapseIOSynapticConnection(childOrganism, synapse.SourceNotification));
                }
                else
                {
                    BindSynapseToRandomNeuron(childOrganism, synapse.SourceNotification as INeuron).OutputSynapse.Add(synapse);
                }
            }
            else
            {
                if (TargetNotification == null)
                    return;

                var synapseticConnection = childOrganism.Cells.Find(TargetNotification.Guid) as INeuron ??
                                                             (ISynapticConnection)childOrganism.GetSynapseIO(TargetNotification.Guid);

                synapse.AddSynapticConnectionTarget(synapseticConnection);
            }
        }

        private void MutateSource(IOrganism childOrganism, double mutationRate, ISynapse synapse)
        {
            // if we are going to mutuate choose between neurons and SyapseIO values
            if (Mutation.MutateState(mutationRate))
            {

                // check against binding name
                if (BindSynapseToSynapseIOInsteadOfNeuron(childOrganism, synapse, synapse.SourceNotification as INeuron))
                {
                    synapse.AddSynapticConnectionSource(GetRandomSynapseIOSynapticConnection(childOrganism, synapse.TargetNotification));
                }
                else
                {
                    BindSynapseToRandomNeuron(childOrganism, synapse.SourceNotification as INeuron).InputSynapse.Add(synapse);
                }
            }
            else
            {
                if (SourceNotification == null)
                    return;

                var synapseticConnection = childOrganism.Cells.Find(SourceNotification.Guid) as INeuron ??
                                                             (ISynapticConnection)childOrganism.GetSynapseIO(SourceNotification.Guid);

                synapse.AddSynapticConnectionSource(synapseticConnection);
            }
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public override void Dispose()
        {
            base.Dispose();
            ActivationEvent = null;
        }

        /// <summary>
        /// To string
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            var sb = new StringBuilder();
            sb.Append("SimpleSynapse" + Environment.NewLine);
            sb.Append("Input value : " + InputValue + Environment.NewLine);
            sb.Append("Output value : " + OutputValue + Environment.NewLine);

            if (SourceNotification != null)
                sb.Append("Source : " + SourceNotification + Environment.NewLine);
            if (TargetNotification != null)
                sb.Append("Target : " + TargetNotification + Environment.NewLine);

            return sb.ToString();
        }
    }
}